﻿// -----------------------------------------------------------------------
// <copyright file="PapService.cs" company="Zen Design Corp">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Zen.BlackBerry.Pap
{
	using System;
	using System.Collections.Specialized;
	using System.Diagnostics;
	using System.IO;
	using System.Net;
	using System.Text;
	using System.Threading.Tasks;
	using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
	using Microsoft.Practices.Unity;
	using Zen.BlackBerry.Common;
	using Zen.BlackBerry.Configuration;
	using Zen.BlackBerry.Pap.Control;
	using Zen.BlackBerry.Pap.Parser;

	/// <summary>
	///	Implementation of the <see cref="IPapService"/> interface.
	/// </summary>
	public class PapService : IPapService
	{
		/// <summary>
		/// Exception handler policy name
		/// </summary>
		/// <remarks>
		/// The default policy is defined as log exception and rethrow
		/// </remarks>
		public static readonly string ExceptionPolicyName = "BPSS Pap";

		private IPushProperties _pushProperties;

		/// <summary>
		/// Initializes a new instance of the <see cref="PapService"/> class.
		/// </summary>
		/// <param name="pushProperties">The push properties.</param>
		public PapService(
			[Dependency]IPushProperties pushProperties)
		{
			_pushProperties = pushProperties;
		}

		/// <summary>
		/// Gets or sets the exception manager.
		/// </summary>
		/// <value>The exception manager.</value>
		[Dependency]
		public ExceptionManager ExceptionManager
		{
			get;
			set;
		}

		/// <summary>
		/// Called by the service to process the specified inbound notification.
		/// </summary>
		/// <param name="notification">
		/// The notification to process.
		/// </param>
		/// <param name="requestParameters">
		/// The parameters associated with the HTTP request.
		/// </param>
		/// <remarks>
		/// Derived classes should override this method to perform actions
		/// based on the result notification.
		/// </remarks>
		protected virtual void OnNotification(
			ResultNotification notification, NameValueCollection requestParameters)
		{
		}

		private void ValidateCredentials(
			string userName, string password, bool isBISPush)
		{
			if (isBISPush)
			{
				if (string.IsNullOrEmpty(userName))
				{
					throw new ArgumentNullException("userName");
				}
				if (string.IsNullOrEmpty(password))
				{
					throw new ArgumentNullException("password");
				}
			}
			else
			{
				// Check we have neither username or password 
				//	OR
				// both the username and password
				if ((string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password)) ||
					(!string.IsNullOrEmpty(userName) && string.IsNullOrEmpty(password)))
				{
					throw new ArgumentException("Username and password should both be null or both non-null.");
				}
			}
		}

		private async Task<TResult> SendPapMessageAndGetResponse<TResult>(
			string userName,
			string password,
			string destination,
			PapRequestMessage message)
			where TResult : PapMessage
		{
			// Get PPG base address and suffix
			string ppgAddressBase, ppgAddressSuffix;
			if (message.Control.IsUsingPublicPush)
			{
				ppgAddressBase = _pushProperties.PublicPpgAddress;
				ppgAddressSuffix = "mms/PD_pushRequest";
			}
			else
			{
				ppgAddressBase = _pushProperties.EnterprisePpgAddress;
				ppgAddressSuffix = "pap";
			}
			if (string.IsNullOrEmpty(ppgAddressBase))
			{
				throw new InvalidOperationException("PPG address is null or empty.");
			}

			// Build final address for message
			Uri ppgUri = new Uri(new Uri(ppgAddressBase), ppgAddressSuffix);

			// Prepare HTTP request
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ppgUri);
			request.Method = "POST";
			request.Headers.Add(HttpRequestHeader.ContentEncoding, "UTF-8");
			request.ContentType = message.ContentTypeForHeader;
			request.AllowWriteStreamBuffering = true;
			request.Timeout = _pushProperties.HttpConnectionTimeout;

			// Pushes to BES require additional headers
			if (!message.Control.IsUsingPublicPush &&
				!string.IsNullOrEmpty(destination))
			{
				request.Headers.Add("X-Rim-Push-Dest-Port", destination);
				request.Headers.Add("X-Rim-Push-Gateways-Req", "true");
			}

			// Setup authorization header
			string rawCredentials = string.Format("{0}:{1}", userName, password);
			string encodedCredentials = string.Format(
				"Basic {0}",
				Convert.ToBase64String(Encoding.UTF8.GetBytes(rawCredentials)));
			request.Headers.Add("Authorization", encodedCredentials);

			// Stuff data payload into request stream
			message.Control.PushProperties = _pushProperties;
			StreamWriter writer = new StreamWriter(
				request.GetRequestStream(), Encoding.UTF8);
			message.WriteTo(writer);
			writer.Flush();
			writer.Close();

			// Issue async request...
			HttpWebResponse response = (HttpWebResponse)
				await request.GetResponseAsync();

			if (response.StatusCode == HttpStatusCode.OK)
			{
				Stream stream = response.GetResponseStream();
				//return (TResult)PapMessageParser.Read(stream);

				MemoryStream memBuffer = new MemoryStream();
				stream.CopyTo(memBuffer);
				memBuffer.Seek(0, SeekOrigin.Begin);
				StreamReader reader = new StreamReader(memBuffer);
				Debug.WriteLine("Response Payload");
				Debug.WriteLine(reader.ReadToEnd());
				memBuffer.Seek(0, SeekOrigin.Begin);
				return (TResult)PapMessageParser.Read(memBuffer);
			}
			else
			{
				throw new InvalidOperationException();
			}
		}

		#region IPapService Members

		/// <summary>
		/// Sends a cancel request using the attributes in the
		/// <paramref name="cancelMessage"/>.
		/// </summary>
		/// <param name="userName">The username of the Push Initiator (for a public (BIS) push, this
		/// would be the application id; for an enterprise (BES) push this
		/// might be null).</param>
		/// <param name="password">The password of the Push Initiator (for a public (BIS) push, this
		/// would be the application password; for an enterprise (BES) push
		/// this might be null).</param>
		/// <param name="cancelMessage">The cancel message attributes needed to construct the PAP request.</param>
		/// <returns>
		/// A task that when complete will contain the PAP response from the
		/// cancel request.
		/// </returns>
		public Task<CancelResponse> Cancel(
			string userName,
			string password,
			CancelMessageControl cancelMessage)
		{
			return ExceptionManager.Process(() =>
				{
					// Sanity checks
					if (cancelMessage == null)
					{
						throw new ArgumentNullException("cancelMessage");
					}
					ValidateCredentials(userName, password, cancelMessage.IsUsingPublicPush);

					// Create message, send to PPG and return the response
					PapRequestMessage message = new PapRequestMessage(cancelMessage);
					return SendPapMessageAndGetResponse<CancelResponse>(
						userName, password, null, message);
				},
				ExceptionPolicyName);
		}

		/// <summary>
		/// Pushes the specified user name.
		/// </summary>
		/// <param name="userName">The username of the Push Initiator (for a public (BIS) push, this
		/// would be the application id; for an enterprise (BES) push this
		/// might be null).</param>
		/// <param name="password">The password of the Push Initiator (for a public (BIS) push, this
		/// would be the application password; for an enterprise (BES) push
		/// this might be null).</param>
		/// <param name="destination">For a public (BIS) push, the application id to push the content to;
		/// for an enterprise (BES) push, the application port to push the
		/// content to.</param>
		/// <param name="pushMessage">The push message attributes needed to construct the PAP request.</param>
		/// <param name="content">The content of the push message.</param>
		/// <returns>
		/// A task that when complete will contain the PAP response from the
		/// push request.
		/// </returns>
		public Task<PushResponse> Push(
			string userName,
			string password,
			string destination,
			PushMessageControl pushMessage,
			Content content)
		{
			return ExceptionManager.Process(() =>
				{
					// Sanity checks
					if (pushMessage == null)
					{
						throw new ArgumentNullException("pushMessage");
					}
					if (content == null)
					{
						throw new ArgumentNullException("content");
					}
					bool isBISPush = pushMessage != null && pushMessage.IsUsingPublicPush;
					ValidateCredentials(userName, password, isBISPush);
					if (!isBISPush)
					{
						// Destination should be a port so attempt to parse
						int destPort;
						if (!Int32.TryParse(destination, out destPort))
						{
							throw new ArgumentException(string.Format(
								"Invalid port number for Enterprise/BES push: {0}.",
								destination), "destination");
						}
					}

					// Create message, send to PPG and return the response
					PapRequestMessage message = new PapRequestMessage(pushMessage, content);
					return SendPapMessageAndGetResponse<PushResponse>(
						userName, password, destination, message);
				},
				ExceptionPolicyName);
		}

		/// <summary>
		/// Sends a status query request using the attributes in the
		/// <paramref name="statusMessage"/>.
		/// </summary>
		/// <param name="userName">The username of the Push Initiator (for a public (BIS) push, this
		/// would be the application id; for an enterprise (BES) push this
		/// might be null).</param>
		/// <param name="password">The password of the Push Initiator (for a public (BIS) push, this
		/// would be the application password; for an enterprise (BES) push
		/// this might be null).</param>
		/// <param name="statusMessage">The status message attributes needed to construct the PAP request.</param>
		/// <returns>
		/// A task that when complete will contain the PAP response from the
		/// status query request.
		/// </returns>
		public Task<StatusQueryResponse> StatusQuery(
			string userName,
			string password,
			StatusQueryMessageControl statusMessage)
		{
			return ExceptionManager.Process(() =>
				{
					// Sanity checks
					if (statusMessage == null)
					{
						throw new ArgumentNullException("statusMessage");
					}
					ValidateCredentials(userName, password, statusMessage.IsUsingPublicPush);

					// Create message, send to PPG and return the response
					PapRequestMessage message = new PapRequestMessage(statusMessage);
					return SendPapMessageAndGetResponse<StatusQueryResponse>(
						userName, password, null, message);
				},
				ExceptionPolicyName);
		}

		/// <summary>
		/// Subscriptions the query.
		/// </summary>
		/// <param name="userName">The username of the Push Initiator (for a public (BIS) push, this
		/// would be the application id; for an enterprise (BES) push this
		/// might be null).</param>
		/// <param name="password">The password of the Push Initiator (for a public (BIS) push, this
		/// would be the application password; for an enterprise (BES) push
		/// this might be null).</param>
		/// <param name="subscriptionMessage">The subscription message attributes needed to construct the PAP
		/// request.</param>
		/// <returns>
		/// A task that when complete will contain the PAP response from the
		/// subscription query request.
		/// </returns>
		public Task<SubscriptionQueryResponse> SubscriptionQuery(
			string userName,
			string password,
			SubscriptionQueryControl subscriptionMessage)
		{
			return ExceptionManager.Process(() =>
				{
					// Sanity checks
					if (subscriptionMessage == null)
					{
						throw new ArgumentNullException("subscriptionMessage");
					}
					ValidateCredentials(userName, password, subscriptionMessage.IsUsingPublicPush);

					// Create message, send to PPG and return the response
					PapRequestMessage message = new PapRequestMessage(subscriptionMessage);
					message.UseRimSpecificRoot = true;
					return SendPapMessageAndGetResponse<SubscriptionQueryResponse>(
						userName, password, null, message);
				},
				ExceptionPolicyName);
		}

		/// <summary>
		/// Called when a result notification message is received from the PPG
		/// (for example by the host service or website).
		/// </summary>
		/// <param name="resultMessage">PAP result notification XML message.</param>
		/// <param name="requestParameters">The HTTP request parameters sent by the PPG.</param>
		/// <returns>
		/// A <see cref="T:ResultNotificationResponseControl"/> object to be
		/// sent to the PPG.
		/// </returns>
		public ResultNotificationResponseControl ReceiveNotification(
			ResultNotification resultMessage,
			NameValueCollection requestParameters)
		{
			ResultNotificationResponseControl response =
				new ResultNotificationResponseControl
				{
					PushProperties = _pushProperties,
					PushId = resultMessage.PushId,
					Address = new Address
						{
							Value = resultMessage.Address
						}
				};
			try
			{
				if (string.IsNullOrEmpty(resultMessage.Address))
				{
					if (resultMessage.SenderName == null ||
						resultMessage.SenderName == "RIM Push-Data Service")
					{
						response.IsUsingPublicPush = true;
					}
					else
					{
						response.IsUsingPublicPush = false;
					}
					throw new PushSDKException("Result notification has null address.");
				}

				// Setup our default response
				response.Code = StatusCode.OK.Code;
				response.Description = "Result notification received";

				string parsedAddress = AddressList.ParseBESAddress(resultMessage.Address);
				if (parsedAddress.Length == resultMessage.Address.Length)
				{
					response.IsUsingPublicPush = true;
				}
				else
				{
					response.IsUsingPublicPush = false;
				}

				// TODO: Validate notification
				//resultMessage.Validate();

				// Now pass notification to derived classes
				OnNotification(resultMessage, requestParameters);
			}
			catch (Exception exception)
			{
				// TODO: Log or pass exception for processing...

				// TODO: Add type safe exception checking so we can provide a
				//	better error description here.
				// TODO: Test inner exception - if we find queue full exception
				//	then change the code to serverfailure...

				// For now we just consider everything to be a bad request
				response.Code = StatusCode.BadRequest.Code;
				response.Description = "Error handling result notification";
			}

			return response;
		}

		#endregion
	}
}
